/*
Copyright (c) 2024 Seldon Technologies Ltd.

Use of this software is governed by
(1) the license included in the LICENSE file or
(2) if the license included in the LICENSE file is the Business Source License 1.1,
the Change License after the Change Date as each is defined in accordance with the LICENSE file.
*/

package server

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"

	"github.com/ghodss/yaml"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"knative.dev/pkg/apis"
	"sigs.k8s.io/controller-runtime/pkg/client"

	mlopsv1alpha1 "github.com/seldonio/seldon-core/operator/v2/apis/mlops/v1alpha1"
	"github.com/seldonio/seldon-core/operator/v2/controllers/reconcilers/common"
	"github.com/seldonio/seldon-core/operator/v2/pkg/constants"
)

const (
	agentConfigMapName   = "seldon-agent"
	scalingConfigMapName = "seldon-scaling"
	kafkaConfigMapName   = "seldon-kafka"
	traceConfigMapName   = "seldon-tracing"
)

type ConfigMapReconciler struct {
	common.ReconcilerConfig
	configMaps []*v1.ConfigMap
}

func NewConfigMapReconciler(
	common common.ReconcilerConfig,
	config *mlopsv1alpha1.SeldonConfiguration,
	meta metav1.ObjectMeta) (*ConfigMapReconciler, error) {

	configMaps, err := toConfigMaps(config, meta)
	if err != nil {
		return nil, err
	}
	return &ConfigMapReconciler{
		ReconcilerConfig: common,
		configMaps:       configMaps,
	}, nil
}

func (s *ConfigMapReconciler) GetResources() []client.Object {
	var objs []client.Object
	for _, svc := range s.configMaps {
		objs = append(objs, svc)
	}
	return objs
}

func toConfigMaps(config *mlopsv1alpha1.SeldonConfiguration, meta metav1.ObjectMeta) ([]*v1.ConfigMap, error) {
	agentConfigMap, err := getAgentConfigMap(config.AgentConfig, meta.Namespace)
	if err != nil {
		return nil, err
	}
	kafkaConfigMap, err := getKafkaConfigMap(config.KafkaConfig, meta.Namespace)
	if err != nil {
		return nil, err
	}
	tracingConfigMap, err := getTracingConfigMap(config.TracingConfig, meta.Namespace)
	if err != nil {
		return nil, err
	}
	scalingConfigMap, err := getScalingConfigMap(config.ScalingConfig, meta.Namespace)
	if err != nil {
		return nil, err
	}
	return []*v1.ConfigMap{
		agentConfigMap,
		kafkaConfigMap,
		tracingConfigMap,
		scalingConfigMap,
	}, nil
}

func getAgentConfigMap(agentConfig mlopsv1alpha1.AgentConfiguration, namespace string) (*v1.ConfigMap, error) {
	agentJson, err := yaml.Marshal(agentConfig)
	if err != nil {
		return nil, err
	}
	return &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      agentConfigMapName,
			Namespace: namespace,
		},
		Data: map[string]string{
			"agent.yaml": string(agentJson),
		},
	}, nil
}

func getScalingConfigMap(scalingConfig mlopsv1alpha1.ScalingConfig, namespace string) (*v1.ConfigMap, error) {
	scalingJson, err := yaml.Marshal(scalingConfig)
	if err != nil {
		return nil, err
	}
	return &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      scalingConfigMapName,
			Namespace: namespace,
		},
		Data: map[string]string{
			"scaling.yaml": string(scalingJson),
		},
	}, nil
}

func getKafkaConfigMap(kafkaConfig mlopsv1alpha1.KafkaConfig, namespace string) (*v1.ConfigMap, error) {
	kafkaJson, err := json.Marshal(kafkaConfig)
	if err != nil {
		return nil, err
	}
	return &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      kafkaConfigMapName,
			Namespace: namespace,
		},
		Data: map[string]string{
			"kafka.json": string(kafkaJson),
		},
	}, nil
}

func getTracingConfigMap(tracingConfig mlopsv1alpha1.TracingConfig, namespace string) (*v1.ConfigMap, error) {
	tracingJson, err := json.Marshal(tracingConfig)
	if err != nil {
		return nil, err
	}
	return &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      traceConfigMapName,
			Namespace: namespace,
		},
		Data: map[string]string{
			"tracing.json":                string(tracingJson),
			"OTEL_JAVAAGENT_ENABLED":      strconv.FormatBool(!tracingConfig.Disable),
			"OTEL_EXPORTER_OTLP_ENDPOINT": fmt.Sprintf("http://%s", tracingConfig.OtelExporterEndpoint),
			"OTEL_EXPORTER_OTLP_PROTOCOL": tracingConfig.OtelExporterProtocol,
		},
	}, nil
}

func (s *ConfigMapReconciler) getReconcileOperation(ctx context.Context, idx int, configMap *v1.ConfigMap) (constants.ReconcileOperation, error) {
	found := &v1.ConfigMap{}
	ctx, cancel := context.WithTimeout(ctx, constants.K8sAPISingleCallTimeout)
	defer cancel()
	err := s.Client.Get(ctx, types.NamespacedName{
		Name:      configMap.GetName(),
		Namespace: configMap.GetNamespace(),
	}, found)
	if err != nil {
		if errors.IsNotFound(err) {
			return constants.ReconcileCreateNeeded, nil
		}
		return constants.ReconcileUnknown, err
	}
	if equality.Semantic.DeepEqual(configMap.Data, found.Data) {
		// Update our version so we have Status if needed
		s.configMaps[idx] = found
		return constants.ReconcileNoChange, nil
	}
	// Update resource vesion so the client Update will succeed
	s.configMaps[idx].SetResourceVersion(found.ResourceVersion)
	return constants.ReconcileUpdateNeeded, nil
}

func (s *ConfigMapReconciler) Reconcile(ctx context.Context) error {
	logger := s.Logger.WithName("ConfigMapReconcile")
	for idx, configMap := range s.configMaps {
		op, err := s.getReconcileOperation(ctx, idx, configMap)
		switch op {
		case constants.ReconcileCreateNeeded:
			logger.V(1).Info("ConfigMap Create", "Name", configMap.GetName(), "Namespace", configMap.GetNamespace())
			err = s.Client.Create(ctx, configMap)
			if err != nil {
				logger.Error(err, "Failed to create configmap", "Name", configMap.GetName(), "Namespace", configMap.GetNamespace())
				return err
			}
		case constants.ReconcileUpdateNeeded:
			logger.V(1).Info("ConfigMap Update", "Name", configMap.GetName(), "Namespace", configMap.GetNamespace())
			err = s.Client.Update(ctx, configMap)
			if err != nil {
				logger.Error(err, "Failed to update configmap", "Name", configMap.GetName(), "Namespace", configMap.GetNamespace())
				return err
			}
		case constants.ReconcileNoChange:
			logger.V(1).Info("ConfigMap No Change", "Name", configMap.GetName(), "Namespace", configMap.GetNamespace())
		case constants.ReconcileUnknown:
			logger.Error(err, "Failed to get reconcile operation for configmap", "Name", configMap.GetName(), "Namespace", configMap.GetNamespace())
			return err
		}
	}
	return nil
}

func (s *ConfigMapReconciler) GetConditions() []*apis.Condition {
	return nil
}
